import { Injectable } from "@angular/core";
import { Device } from '@ionic-native/device';
import { SqliteUtil } from "./sqlite.util";
import { ObjectDto, ObjectDtoInterface } from "./object.dto";
import { VerifyService } from "./verify.service";
import { JsonMappingUtil } from "../../models/json-mapping-util";
import './../array-extension.ts';
import { Observable } from "rxjs";
import { Subscriber } from "rxjs/Subscriber";
import { ConditionFetch, ConditionPair } from "./condition-fetch";
import { SqliteDbTransction } from "./sqlite-transaction";
import {SyncStatus} from "../../models/sync-status";
import {SecureStorageService} from "../secure-storage.services";


declare var window: {
  plugin: object,
  alert: any,
  indexedDB: any,
  openDatabase: any,
  deleteDatabase: any,
  sqlitePlugin: any,
  localStorage: {
    getItem: any,
    setItem: any,
    removeItem: any
  }
};

// console.log(window.openDatabase);

export interface TableInterface {
  tableName: string;
  tableConstructor: object;
}

let gSqliteService: SqliteService;

@Injectable()
export class SqliteService {
  dbUtil: SqliteUtil = new SqliteUtil();
  private sqlite: any = null;

  constructor(private device: Device,
              private secureStorageService: SecureStorageService
  ) {
    if (gSqliteService) {
      return gSqliteService;
    }
    gSqliteService = this;
    console.log("------------------------------");
  }

  getDb(userid: string = "wudang") {
    if (this.device.cordova) {
      return new Promise((resolve) => {
        // resolve(this.sqlite.create({
        //     name: 'wudang',
        //     location: 'default',
        // }));
        // key: 'wudangsqlite'
        console.log(userid, "wo shi sqliteName");
        if (!this.sqlite) {
          this.sqlite = window.sqlitePlugin.openDatabase({
            name: userid,
            // key: 'wudangsqlite',
            location: 'default'
          });
        }
        // let localStorageTables;
        // if (window.localStorage.getItem("databaseNames")) {
        //   localStorageTables = JSON.parse(window.localStorage.getItem("databaseNames"));
        //   if (localStorageTables.indexOf(userid) === -1) {
        //     localStorageTables.push(userid);
        //   }
        // } else {
        //   localStorageTables = [];
        //   localStorageTables.push(userid);
        // }
        // window.localStorage.setItem("databaseNames", JSON.stringify(localStorageTables));
        resolve(this.sqlite);
      });
    } else {
      if (!window.openDatabase) {
        window.alert("Your browser doesn't support a stable version of openDatabase. Such and such feature will not be available.");
      } else {
        return new Promise((resolve, reject) => {
          if (!this.sqlite) {
            this.sqlite = window.openDatabase(userid, '1.0', 'chrome DB', 20 * 1024 * 1024);
          }
          resolve(this.sqlite);
        });
      }
    }
  }

  deleteDataBase() {
    return this.dropDataBase();
  }

  resetDataBase() {
    this.sqlite = null;
  }

  executeSqlWithResults<T extends ObjectDto>(sql: string, constructFunc: { new(): T }) {
    return new Promise((resolve, reject) => {
      this.operate({
        sql: sql,
        param: [],
        model: new constructFunc()
      })
        .then((results) => {
          try {
            console.log(results);
            let rows = Array.from(results['results']["rows"]);
            resolve(rows.map((row) => {
              let objDto = new constructFunc();
              JsonMappingUtil.MappingAllProp(row, objDto);
              return objDto;
            }));
          } catch (e) {
            resolve([]);
          }
        }).catch((e) => {
          reject(e);
        });
    });
  }

  executeSql(sql: string) {
    return this.tableOperate({ sql });
  }

  transactionFn(db, cb) {
    return new Promise((resolve, reject) => {
      db.transaction(cb, (e) => {
        console.error(e);
        // reject(e);
      }, (e) => {
        resolve(e);
      });
    });
  }

  executeSqlFn(sqlite, sql, param) {
    return new Promise((resolve, reject) => {
      console.log(sql);
      sqlite.executeSql(sql, param, (text, results) => {
        let res = [];
        if (this.device.cordova) {
          if (results["rows"].length > 0) {
            for (let i = 0; i < results["rows"].length; i++) {
              res.push(results["rows"]["item"](i));
            }
          }
          results["rows"] = res;
        }
        resolve({
          tx: text,
          results: results
        });
      }, (e) => {
        // console.error(e);
        reject(e);
      });
    });
  }

  operate(opt) {
    let { sql, param } = opt;
    return new Promise((resolve, reject) => {
      this.transactionFn(this.sqlite, (tx) => {
        if (sql) {
          this.executeSqlFn(tx, sql, param)
            .then((parameter) => {
              resolve(parameter);
            }).catch((e) => {
              console.error(`execute ${sql} error, please check your code!`);
              reject(e);
            });
        }
        else {
          reject("invalid sql: " + sql);
        }
      });
    });

  }

  tableOperate(opt) {
    let { sql } = opt;

    return new Promise((resolve, reject) => {
      // this.secureStorageService.get("loginAccount").subscribe((account) => {
      let account;
      if (this.device.cordova) {
        account = window.localStorage.getItem('loginAccount');
      }
      this.getDb(account)
        .then((db: any) => {
          this.transactionFn(db, (tx) => {
            tx.executeSql(sql);
          })
            .then((e) => {
              resolve();
            });
        })
        .catch(
          () => {
            reject();
          }
        );
      // });
    });
  }

  creatTable<T extends ObjectDto>(modelClass: { new(): T; }) {
    let model = new modelClass();
    let table = model.table();
    if (!this.device.cordova) {
      let localStorageTables;
      if (window.localStorage.getItem("tableNames")) {
        localStorageTables = JSON.parse(window.localStorage.getItem("tableNames"));
        localStorageTables.push(table.tableName);
      } else {
        localStorageTables = [];
        localStorageTables.push(table.tableName);
      }
      window.localStorage.setItem("tableNames", JSON.stringify(localStorageTables));
    }
    return this.tableOperate({
      sql: `create table if not exists ${table.tableName} (${this.dbUtil.objToString(table.tableConstructor)})`
    });
  }
  /*
  this.dbService.dropTable(Product);
   */
  dropTable<T extends ObjectDto>(modelClass: { new(): T; }) {
    let model = new modelClass();
    let table = model.table();
    return this.tableOperate({
      sql: `drop table ${table.tableName}`
    });
  }

  dropDataBase() {
    if (this.device.cordova) {
      // return Observable.combineLatest(
      //   this.secureStorageService.get("loginAccount")
      // )
      //   .flatMap((account) => {
      //     return Observable.fromPromise(new Promise((resolve, error) => {
      //       window.sqlitePlugin.deleteDatabase({name: account, location: 'default'}, (data) => {
      //         this.sqlite = null;
      //         resolve(data);
      //       }, (err) => {
      //         error(err);
      //       });
      //     }));
      //   });
      return Observable.fromPromise(new Promise((resolve, error) => {
        window.sqlitePlugin.deleteDatabase({name: window.localStorage.getItem("loginAccount"), location: 'default'}, (data) => {
          this.sqlite = null;
          resolve(data);
        }, (err) => {
          error(err);
        });
      }));
      } else {
        let tableNamesStr = window.localStorage.getItem("tableNames");
        let tableNames: Array<any> = tableNamesStr ? JSON.parse(tableNamesStr) : [];
        tableNames = tableNames.map((tableName) => {
          return Observable.fromPromise(this.tableOperate({
            sql: `drop table ${tableName}`
          }));
        });
        // console.log(tableNames);
        if (tableNames.length > 0) {
          return Observable.combineLatest(tableNames);
        }
        else {
          return Observable.create((ob) => {
            ob.next([]);
            ob.complete();
          });
        }

      }
  }

  // sqlite 不支持多列添加
  /*
      this.sqliteService.addColumn(Product, {
          imgss: "text"
      });
   */
  addColumn<T extends ObjectDto>(modelClass: { new(): T; }, opt) {
    let model = new modelClass();
    let table = model.table();
    return this.tableOperate({
      sql: `alter table ${table.tableName} add ${this.dbUtil.objToString(opt)}`
    });
    // return this.tableOperate(`alter table ${table.tableName} add log text`);
  }

  isHaveColumn(tableName, column) {
    // this.tableOperate('select count(*) from information_schema.columns where table_name = ' + tableName + ' and column_name = ' + column);
    // this.tableOperate('select * from syscolumns where id=object_id(' + tableName + ') and name=' + column);
    // this.tableOperate('select  count(*)  from  dbo.sysobjects where name=  ' + tableName);
    // this.tableOperate('select count(*) from user_objects where  object_name = ' + tableName);
  }

  add<T extends ObjectDto>(model: T, keys: Array<string> = []) {
    let table = model.table();
    let originData = new VerifyService().verifyColumn(model, keys);
    let needAddData = this.dbUtil.splitObj(table, originData);
    let cols = needAddData.keys.join(",");
    let values = needAddData.values.join(",");
    return this.operate({
      sql: `insert into ${table.tableName} ( ${cols} ) values ( ${values} )`,
      param: [],
      model: model
    });
  }

  addRows<T extends ObjectDto>(dtos: Array<T>, keys: Array<string> = [], size: number = 10) {
    let table = dtos[0].table();
    let pages = dtos.chunk(size);

    let observalbes = pages.map((pageDtos: Array<T>) => {
      return this.addOnePageRows(pageDtos, keys);
    });
    return Observable.combineLatest(observalbes).map(() => true);
  }

  addOnePageRows<T extends ObjectDto>(dtos: Array<T>, keys: Array<string> = []) {
    let table = dtos[0].table();
    let verifyService = new VerifyService();

    //
    // batch insert.
    // insert into tableName(col1, col2, col3, ...) values (val11, val12, val13, ...),..., (val21, val22, val23, ...);
    //  we assume all the rows data use same columns.
    //

    // let firstDtoData = verifyService.verifyColumn(dtos[0], keys);
    // let firsDtotData = this.dbUtil.splitObj(table, firstDtoData);

    // let sql = `insert into ${table.tableName} (${firsDtotData.keys.join(',')}) `;
    let sqls = dtos.map((dto: T, index: number) => {
      // if(index === 0) {
      //   return `values ( ${firsDtotData.values.join(',')} )`;
      // }

      let dtoData = verifyService.verifyColumn(dto, keys);
      let generatedSqlColumnsAndValues = this.dbUtil.splitObj(table, dtoData);
      return `insert into ${table.tableName} (${generatedSqlColumnsAndValues.keys.join(',')}) ` +
      `values ( ${generatedSqlColumnsAndValues.values.join(',')} )`;
    });
    return new SqliteDbTransction(this.sqlite, sqls).commit();
  }

  clear<T extends ObjectDto>(modelClass: { new(): T; }, and: Array<string> = [], or: Array<string> = []) {
    let model = new modelClass();
    let table = model.table();
    let condition = this.dbUtil.whereSplice(and, or);
    return this.operate({
      sql: `delete from ${table.tableName} ${condition} `,
      param: [],
      model: model
    })
      .then((results) => {
        console.log(JSON.stringify(results['results']));
      });
  }

  remove<T extends ObjectDto>(modelClass: { new(): T; }, obj: ConditionFetch = {
    and: {},
    or: []
  }) {
    let condition = this.dbUtil.where(modelClass, obj);
    this.clear(modelClass, condition.and, condition.or);
  }

  modify<T extends ObjectDto>(modelClass: { new(): T; }, update: object, and: Array<string> = [], or: Array<string> = []) {
    let model = new modelClass();
    let table = model.table();
    let updateCondition = this.dbUtil.conditionSplice(this.dbUtil.splitAndSplice(this.dbUtil.splitObj(table, update)), ',');
    let condition = this.dbUtil.whereSplice(and, or);
    return this.operate({
      sql: `update ${table.tableName} set ${updateCondition} ${condition}`,
      param: [],
      model: model
    });
  }

  updateRow<T extends ObjectDto>(dto: T, filterKeyValue: ConditionPair): Observable<boolean> {
    let tableInfo = dto.table();
    let nonEmptyDataObj = this.dbUtil.filterEmptyAttr(dto);
    let whereConditions = this.dbUtil.whereKeyValues(
      dto, { and: filterKeyValue });
    let where = this.dbUtil.whereSplice(whereConditions.and, whereConditions.or);
    let sqlSetValues = this.dbUtil.conditionSplice(this.dbUtil.splitAndSplice(this.dbUtil.splitObj(tableInfo, nonEmptyDataObj)), ',');
    console.warn(`update ${tableInfo.tableName} set ${sqlSetValues} ${where}`);

    let sql = `update ${tableInfo.tableName} set ${sqlSetValues} ${where}`;
    return Observable.fromPromise(this.executeSql(sql)).map(() => dto).map(() => true);
  }

  updateRows<T extends ObjectDto>(
    dtos: Array<T>,
    filterKeys: Array<string>,
    size: number = 10): Observable<boolean> {

    let table = dtos[0].table();
    let pages = dtos.chunk(size);

    let observables = pages.map((pageDtos: Array<T>) => {
      return this.updateOnePageRows(pageDtos, filterKeys);
    });
    if (observables.length > 1) {
      //
      // TODO observables might be very large, change this to make sure its size is less than 100.
      //
      return Observable.combineLatest(observables).map(() => true);
    }
    return observables.length > 0 ? observables[0] : Observable.of(true);
  }

  beginTransaction() {
    return new SqliteDbTransction(this.sqlite);
  }


  updateOnePageRows<T extends ObjectDto>(
    dtos: Array<T>,
    filterKeys: Array<string>): Observable<boolean> {


    let sqls = dtos.map((dto: T) => {
      let tableInfo = dto.table();
      let nonEmptyDataObj = this.dbUtil.filterEmptyAttr(dto);


      let or = filterKeys.map((key) => {
        let obj = {};
        obj[key] = dto[key];
        return obj;
      });
      let and = {};
      if (or.length > 0) {
        or.forEach((item) => {
          Object.assign(and, item);
        });
      }
      let whereConditions = this.dbUtil.whereKeyValues(dto, { and: and });
      let where = this.dbUtil.whereSplice(whereConditions.and, whereConditions.or);
      let sqlSetValues = this.dbUtil.conditionSplice(this.dbUtil.splitAndSplice(this.dbUtil.splitObj(tableInfo, nonEmptyDataObj)), ',');
      console.warn(`update ${tableInfo.tableName} set ${sqlSetValues} ${where}`);

      return `update ${tableInfo.tableName} set ${sqlSetValues} ${where}`;
    });
    return new SqliteDbTransction(this.sqlite, sqls).commit();
  }

  observaleFromexecuteSql(sql: string): Observable<boolean> {
    return Observable.fromPromise(this.operate({ sql, param: [] })).map(() => true);
  }


  update<T extends ObjectDto>(param: {
    dto: { new(): T; },
    dtoObject: T,
    filter?: {
      and?: object,
      or?: Array<ConditionPair>
    }
  }) {
    let updateData = this.dbUtil.filterEmptyAttr(param.dtoObject);
    let condition = this.dbUtil.where(param.dto, param.filter);
    return this.modify(param.dto, updateData, condition.and, condition.or);
  }

  simpleFetch<T extends ObjectDto>(dtoConstructFunc: { new(): T; }, returnedPropertyKeys: Array<string> = []) {
    let model = new dtoConstructFunc();
    let table = model.table();
    let originData = new VerifyService().verifyColumn(model, returnedPropertyKeys);
    let columns = this.dbUtil.splitObj(table, originData);
    let key;
    if (returnedPropertyKeys.length > 0) {
      key = columns.keys.length <= 0 ? "*" : columns.keys.join(",");
    } else {
      key = "*";
    }
    return new Promise((resolve, reject) => {
      this.operate({
        sql: `select ${key} from ${table.tableName}`,
        param: [],
        model: model
      }).then((results) => {
        try {
          resolve({
            tx: results["tx"],
            results: results['results']["rows"]
          });
        } catch (e) {
          resolve({
            tx: null,
            results: null
          });
        }
      }).catch((e) => {
        reject(e);
      });
    });
  }

  fetch<T extends ObjectDto>(modelClass: { new(): T; }, obj: ConditionFetch, arr: Array<string> = []) {
    let condition = this.dbUtil.where(modelClass, obj);
    return this.find(modelClass, obj.filter, condition.like, condition.limit, condition.and, condition.or, condition.in, arr);
  }

  find<T extends ObjectDto>(
    modelClass: { new(): T; },
    filter: Array<string> = [],
    like: Array<string> = [],
    limit: string,
    and: Array<string> = [],
    or: Array<string> = [],
    inCondition: string,
    order: Array<string> = []) {
    let model = new modelClass();
    let table = model.table();
    let condition = this.dbUtil.whereSplice(and, or, like, limit, order, inCondition);
    // condition += ` updateTime like "232"`;
    let key;
    if (filter.length > 0) {
      let originData = new VerifyService().verifyColumn(model, filter);
      let columns = this.dbUtil.splitObj(table, originData);
      key = columns.keys.length <= 0 ? "*" : columns.keys.join(",");
    } else {
      key = "*";
    }
    return new Promise((resolve, reject) => {
      this.operate({
        sql: `select ${key} from ${table.tableName}  ${condition}`,
        param: [],
        model: model
      })
        .then((results) => {
          try {
            resolve({
              tx: results["tx"],
              results: results['results']["rows"]
            });
          } catch (e) {
            resolve({
              tx: null,
              results: null
            });
          }
        }).catch((e) => {
          reject(e);
        });
    });
  }

}
